package datacollectionator;

import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingWorker;

public class LoginDialog {
	
	private JFrame dialog;
	
	private JTextField userNameTf;
	
	private TimedPasswordField passwordField;
	
	public LoginDialog() {
		JLabel userNameLbl = new JLabel("User Name:");
		userNameTf = new JTextField(20);
		
		JLabel passwordLbl = new JLabel("Password:");
		passwordField = new TimedPasswordField();
		passwordField.setColumns(20);
		
		final JButton logInBtn = new JButton("Log In");
		final JButton cancelBtn = new JButton("Cancel");
		final JButton newUserBtn = new JButton("New User");
		final JButton resetBtn = new JButton("Reset");
		
		dialog = new JFrame("Log In");
		
		JPanel userPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
		JPanel passPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
		JPanel userPassPanel = new JPanel(new GridLayout(0, 1));
		JPanel buttonsPanel = new JPanel(new GridLayout(1, 0, 5, 0));
		JPanel southPanel = new JPanel(new BorderLayout(5, 0));
		JPanel contentPane = new JPanel(new BorderLayout(0, 5));
		contentPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
		
		contentPane.add(userPassPanel, BorderLayout.NORTH);
			userPassPanel.add(userPanel);
				userPanel.add(userNameLbl);
				userPanel.add(userNameTf);
				userPanel.add(newUserBtn);
			userPassPanel.add(passPanel);
				passPanel.add(passwordLbl);
				passPanel.add(passwordField);
				passPanel.add(resetBtn);
		contentPane.add(southPanel, BorderLayout.SOUTH);
			southPanel.add(buttonsPanel, BorderLayout.WEST);
				buttonsPanel.add(logInBtn);
				buttonsPanel.add(cancelBtn);
		
		dialog.setContentPane(contentPane);
		dialog.pack();
		dialog.setLocationRelativeTo(null);
		
		ActionListener logInAction = new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				(new SwingWorker<Void, Void>() {
					@Override
					protected Void doInBackground() {
						LoginDialog.this.tryLogIn();
						return null;
					}
					@Override
					protected void done() {
						passwordField.reset();
					}
				}).execute();
			}
		};
		
		cancelBtn.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				dialog.dispose();
			}
		});
		
		newUserBtn.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				(new SwingWorker<Void, Void>() {
					@Override
					protected Void doInBackground() throws IOException {
						String userName = userNameTf.getText();
						String errStr = validateUserName(userName);
						if (errStr != null) {
							JOptionPane.showMessageDialog(dialog, errStr, "Invalid Username", JOptionPane.ERROR_MESSAGE);
							return null;
						}
		
						char[] password = passwordField.getPassword();
						errStr = validatePassword(password);
						if (errStr != null) {
							JOptionPane.showMessageDialog(dialog, errStr, "Invalid Password", JOptionPane.ERROR_MESSAGE);
							return null;
						}

						try {
							TimedPasswordFile.allocate(userName, userName, password, new int[] {password.length - 1, password.length}, Config.DEFAULT_HISTORY_LENGTH);
						} catch (IOException ex) {
							JOptionPane.showMessageDialog(dialog, errStr, "Unexpected error while creating user.", JOptionPane.ERROR_MESSAGE);
							throw ex;
						}
						
						passwordField.reset();
						
						JOptionPane.showMessageDialog(dialog, "User successfully created.", "User Created", JOptionPane.INFORMATION_MESSAGE);
						
						return null;
					}
				}).execute();
			}
		});
		
		logInBtn.addActionListener(logInAction);
		passwordField.addActionListener(logInAction);
		
		resetBtn.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				passwordField.reset();
			}
		});
	}
	
	public void show() {
		dialog.setVisible(true);
	}
	
	private void tryLogIn() {
		String userName = userNameTf.getText();
		TimedPasswordFile passwordFile;
		try {
			passwordFile = TimedPasswordFile.read(userName);
		} catch (FileNotFoundException ex) {
			JOptionPane.showMessageDialog(dialog, "A user with this name does not exist.", "Log In Error", JOptionPane.ERROR_MESSAGE);
			return;
		} catch (IOException ex) {
			JOptionPane.showMessageDialog(dialog, "Error reading password file.", "Log In Error", JOptionPane.ERROR_MESSAGE);
			ex.printStackTrace();
			return;
		}
		
		String fileUserName = passwordFile.getUserName();
		if (!userName.equals(fileUserName)) {
			JOptionPane.showMessageDialog(dialog, "No user account with this name exists.", "Log In Error", JOptionPane.ERROR_MESSAGE);
			return;
		}
		
		char[] testPassword = passwordField.getPassword();
		char[] correctPassword = passwordFile.getPassword();
		if (!validatePassword(correctPassword, testPassword)) {
			JOptionPane.showMessageDialog(dialog, "The password you typed is incorrect.", "Log In Error", JOptionPane.ERROR_MESSAGE);
			return;
		}
		
		KeyTimingDataSequence timingData = passwordField.getKeyTimingData();
		
		KeyData[] keyPressData = timingData.getKeyPresses();
		KeyData[] keyReleaseData = timingData.getKeyReleases();
		
		long[][] testData = new long[2][0];
		testData[0] = calculateKeyDownToKeyDownTimes(keyPressData);
		testData[1] = calculateKeyPressTimes(keyPressData, keyReleaseData);

		Logger logger;
		try {
			logger = Logger.instance();
		} catch (Exception ex) {
			ex.printStackTrace();
			return;
		}

		// log raw key data
		try {
			logger.write("User=");
			logger.write(userName);
			logger.newLine();
			logger.write("Password=");
			logger.write(new String(correctPassword));
			logger.newLine();

			logger.write("Key Presses");
			logger.newLine();
			for (KeyData kd : keyPressData) {
				logger.write(String.format("%d %d", kd.getKeyCode(), kd.getTime()));
				logger.newLine();
			}

			logger.write("Key Releases");
			logger.newLine();
			for (KeyData kd : keyReleaseData) {
				logger.write(String.format("%d %d", kd.getKeyCode(), kd.getTime()));
				logger.newLine();
			}
		} catch (IOException ex) {
			ex.printStackTrace();
		}
		
		if (passwordFile.isReady()) {
			ArrayList<long[][]> trainingData;
			try {
				logger.write("Password Hardening ON.");
				logger.newLine();
			} catch (IOException ex) {
				ex.printStackTrace();
			}
			
			try {
				trainingData = passwordFile.readAllData();
			} catch (IOException ex) {
				JOptionPane.showMessageDialog(dialog, "Error reading password file.", "Log In Error", JOptionPane.ERROR_MESSAGE);
				ex.printStackTrace();
				return;
			}
			
			if (validateKeyDynamics(trainingData, testData)) {
				JOptionPane.showMessageDialog(dialog, "Password Accepted! (Password Hardening ON)", "Log In Error", JOptionPane.INFORMATION_MESSAGE);
				try {
					logger.write("User Accepted.");
					logger.newLine();
				} catch (IOException ex) {
					ex.printStackTrace();
				}
			} else {
				JOptionPane.showMessageDialog(dialog, "Password Rejected! Typing dynamics do not match..", "Log In Error", JOptionPane.ERROR_MESSAGE);
				try {
					logger.write("User Rejected.");
					logger.newLine();
					logger.close();
				} catch (Exception ex) {
					ex.printStackTrace();
				}
				return;
			}
		} else {
			JOptionPane.showMessageDialog(dialog, "Password Accepted! (Password Hardening OFF)", "Log In Error", JOptionPane.INFORMATION_MESSAGE);
			try {
				logger.write("Password Hardening OFF.");
				logger.newLine();
				logger.write("User Accepted.");
				logger.newLine();
			} catch (IOException ex) {
				ex.printStackTrace();
			}
		}
		
		try {
			passwordFile.writeKeyData(testData);
		} catch (IOException ex) {
			ex.printStackTrace();
		}

		try {
			logger.close();
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}
	
	/**
	 * Compare key timings with the history of key timings for this user.
	 * 
	 * @param trainingData Each element of the ArrayList corresponds to 1 history entry for the user. Each history entry is a two dimensional
	 *                     array of longs, where the first dimension corresponds to a particular feature, and the second dimension to a particular
	 *                     data point in that feature. For now, the two features in use are keydown-to-keydown time and keypress duration.
	 * @param testData The data collected for a user's login attempt. It is organized in the same manner as one of the history entries in trainingData.
	 * 
	 * @return True if the user should be accepted, false if they should be rejected
	 */
	private static boolean validateKeyDynamics(ArrayList<long[][]> trainingData, long[][] testData) {
		int numTrainings = trainingData.size();
		int numKeyDown = trainingData.get(0)[0].length;
		int numKeyPress =  trainingData.get(0)[1].length;
		
		long[][] trainingKeyDowns = new long [numTrainings][numKeyDown];
		long[][] trainingKeyPresses = new long [numTrainings][numKeyPress];
		
		// make the training data arrays
		for (int i = 0; i < numTrainings; ++i) {
		    for (int j = 0; j < numKeyDown; ++j) {
		        trainingKeyDowns[i][j] = trainingData.get(i)[0][j];    
		    }
		    for (int j = 0; j < numKeyPress; j++) {
		        trainingKeyPresses[i][j] = trainingData.get(i)[1][j];
		    }
		}
		long[] testKeyDown = new long [numKeyDown];
		long[] testKeyPress = new long [numKeyPress];
		
		System.arraycopy(testData[0], 0, testKeyDown, 0, numKeyDown);
		System.arraycopy(testData[1], 0, testKeyPress, 0, numKeyPress);
		
		double totalProb = clusterProbability(trainingKeyDowns, testKeyDown) * clusterProbability(trainingKeyPresses, testKeyPress);
		
		Logger logger = null;
		try {
			logger = Logger.instance();
			logger.write(String.format("Probability=%f", totalProb));
			logger.newLine();
		} catch (IOException ex) {
			ex.printStackTrace();
		} finally {
			if (logger != null) {
				try {
					logger.close();
				} catch (Exception ex) {
					ex.printStackTrace();
				}
			}
		}
		
        return (totalProb >= .75);
	}
        
    private static double clusterProbability(long[][] training, long[] test) {
        int numTrainings = training.length;
        int numFeatures = training[0].length;
        double[] total = new double [numFeatures];
        double[] center = new double [numFeatures];
        double[] diff = new double [numFeatures];
        double[] std = new double [numFeatures];
        boolean[] pass = new boolean [numFeatures];
        
        //calculate total sum of each feature
        for (int i = 0; i < numTrainings; i++) {
            total[0] += training[i][0];
            for (int j = 1; j < numFeatures; j++) {
                total[j] += training[i][j];
            }
        }

        //calculate the center of the training
        for (int i = 0; i < numFeatures; i++) {
            center[i] = total[i] / (double) numTrainings;
        }

        // calculate std of features
        for (int i = 0; i < numFeatures; i++) {
            for (int j = 0; j < numTrainings; j++) {
                std[i] += Math.pow(training[j][i] - center[i], 2);
            }
            std[i] =  Math.sqrt(std[i] / (double) numTrainings);
        }

        // calculate difference
        diff[0] = Math.abs(center[0] - test[0]);
        for (int i = 1; i < numFeatures; i++) {
            diff[i] = Math.abs(center[i] - test[i]);
        }

        // find the max number of standard deviations away a single element is in the test set
        double maxNumSTD = 0;
        for (int i = 0; i < numTrainings; i++) {
            for (int j = 0; j < numFeatures; j++) {
                if ((Math.abs(training[i][j] - center[j]) / std[j]) > maxNumSTD) {
                    maxNumSTD = (Math.abs(training[i][j] - center[j]) / std[j]); 
                }
            }
        }

        // check if passing 
        int totalPass = 0;
        for (int i = 0; i < numFeatures; i++) {
            if (maxNumSTD * std[i] >= diff[i]) {
                pass[i] = true;
                totalPass++;
            } else {
                pass[i] = false;
            }
        }
        return ((double) totalPass / (double) numFeatures);
    }
	
	private static boolean validatePassword(char[] correctPassword, char[] testPassword) {
		if (correctPassword.length != testPassword.length) {
			return false;
		} else {
			for (int i = 0; i < correctPassword.length; ++i) {
				if (correctPassword[i] != testPassword[i]) {
					return false;
				}
			}
			return true;
		}
	}
	
	private static long[] calculateKeyDownToKeyDownTimes(KeyData[] keyPressData) {
		long[] result = new long[keyPressData.length - 1];
		for (int i = 0; i < result.length; ++i) {
			result[i] = keyPressData[i + 1].getTime() - keyPressData[i].getTime();
		}
		return result;
	}

	private static long[] calculateKeyPressTimes(KeyData[] keyPressData, KeyData[] keyReleaseData) {
//TODO this is a naive implementation that assumes former keys are released before the next key is pressed
		long[] result = new long[keyPressData.length];
		for (int i = 0; i < result.length; ++i) {
			result[i] = keyReleaseData[i].getTime() - keyPressData[i].getTime();
		}
		return result;
	}
	
	private static String validateUserName(String userName) {
		if (!(userName.matches("[a-z]+\\.[1-9]+") || userName.matches("[a-z]+\\.[a-z]+"))) {
			return "Not a valid Wright State email address.";
		}
		return null;
	}
	
	private static String validatePassword(char[] password) {
		if (password.length < 10) {
			return "Password must be at least 10 characters long.";
		}
		return null;
	}
}
